﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections;
using Odyssey.Utils;
using Odyssey.Controls;
using System.Collections.Specialized;
using System.ComponentModel;

namespace Odyssey.ViewModels
{
    /// <summary>
    /// Abstract base class for all DataSources.
    /// </summary>
    /// <typeparam name="T">Type of item.</typeparam>
    public abstract class DataSourceBase<T> : ICollectionChangedListener, IWeakPropertyChangedEventListener, INotifyPropertyChanged
    {
        public DataSourceBase()
            : base()
        {
            Items = new ObservableCollection<object>();
            readonlyItems = new ReadOnlyObservableCollection<object>(Items);

            Type t = this.GetType().GetGenericArguments()[0];
            CanNotify = typeof(INotifyPropertyChanged).IsAssignableFrom(t);

        }

        public DataSourceBase(IEnumerable<T> items, IComparer<T> comparer = null, bool isgroupingEnabled = true, Func<T, bool> filter = null)
            : this()
        {
            this.source = items;
            this.Filter = filter;
            this.IsGroupingEnabled = isgroupingEnabled;
            Comparer = comparer;
            if (items != null)
            {
                Populate(items, comparer);
                NotifyProps(0);
            }

            WeakCollectionChangedListener listener = WeakCollectionChangedListener.CreateIfNecessary(items, this);
            IsObserved = listener != null;
        }


        ~DataSourceBase()
        {
            Unsubscribe();
            source = null;
        }

        public string Name { get; set; }

        private IEnumerable<T> source;
        private Dictionary<string, GroupItem> groups = new Dictionary<string, GroupItem>();

        /// <summary>
        /// Gets the collection of filtered items.
        /// </summary>
        protected ObservableCollection<object> Items { get; private set; }
        private ReadOnlyObservableCollection<object> readonlyItems;


        /// <summary>
        /// Gets whether the collection from which this data source is created has items which can notify propery changes.
        /// </summary>
        public bool CanNotify { get; private set; }

        /// <summary>
        /// Gets whether this data source is created from an other collection which is being observed to reflect the changes.
        /// </summary>
        public bool IsObserved { get; private set; }

        public IDictionary<string, GroupItem> Groups { get { return groups; } }

        /// <summary>
        /// Gets the filter which applies to each item.
        /// </summary>
        public Func<T, bool> Filter { get; private set; }


        /// <summary>
        /// Gets the comparer to be used when adding an item to the collection.
        /// </summary>
        public IComparer<T> Comparer { get;  set; }


        /// <summary>
        /// Gets the collection that contains both items and headers.
        /// </summary>
        public IList View { get { return readonlyItems; } }

        /// <summary>
        /// Gets the number of items, excluding the number of headers.
        /// </summary>
        public int Count
        {
            get { return Items.Count - groups.Count; }
        }

        public bool IsEmpty
        {
            get { return Count == 0; }
        }

        /// <summary>
        /// Gets whether indexing is enabled.
        /// </summary>
        public bool IsGroupingEnabled { get; private set; }


        /// <summary>
        /// Adds an item to the collection.
        /// </summary>
        /// <param name="item"></param>
        public bool Add(T item)
        {
            if (Filter == null || Filter(item))
            {
                int prevCount = Count;
                PrepareItem(item);
                if (IsGroupingEnabled)
                {
                    GroupItem group = GetGroupItem(item);
                    int groupIndex;
                    if (group == null)
                    {
                        string key = GetGroupName(item);
                        group = new GroupItem(key);
                        groups.Add(key, group);
                        groupIndex = GetInsertGroupIndex(key);
                        Items.Insert(groupIndex, group);
                    }
                    else
                    {
                        groupIndex = Items.IndexOf(group);
                    }
                    group.ChildCount++;
                    int insertIndex = GetInsertIndex(groupIndex, group, item);
                    Items.Insert(insertIndex, item);
                }
                else
                {
                    int insertIndex = GetInsertIndex(item);
                    Items.Insert(insertIndex, item);
                }
                NotifyProps(prevCount);
                return true;
            }
            else return false;
        }

        /// <summary>
        /// Updates the specified item. Depending on the Filter(item) result, it will be either removed or added.
        /// </summary>
        /// <param name="item">Item to update.</param>
        public void Update(T item)
        {
            var filter = Filter;
            if (filter != null)
            {
                if (!filter(item))
                {
                    Remove(item);
                }
                else if (!Contains(item))
                {
                    Add(item);
                }
            }
        }

        public bool Reorder(T item)
        {
            var comparer = Comparer;
            if (comparer != null)
            {
                bool isOutOfOrder = GetIsOutOfOrder(item);
                if (isOutOfOrder)
                {
                    Remove(item);
                    Add(item);
                    return true;
                }
            }
            return false;
        }

        private bool GetIsOutOfOrder(T item)
        {
            if (Count > 1)
            {
                int itemIndex = Items.IndexOf(item);
                if (itemIndex >= 0)
                {
                    int groupIndex = FindGroupBeforeItem(itemIndex);

                    GroupItem group = groupIndex >= 0 ? Items[groupIndex] as GroupItem : null;
                    if (group != null && (group.GroupName != GetGroupName(item)))
                    {
                        return true;
                    }
                    else if (groupIndex >= 0)
                    {
                        int index = itemIndex - groupIndex;
                        if (index > 1)
                        {
                            T prev = (T)Items[groupIndex + index - 1];
                            if (Comparer.Compare(prev, item) > 0) return true;
                        }
                        if (index < (group.ChildCount - 2))
                        {
                            T next = (T)Items[groupIndex + index + 1];
                            if (Comparer.Compare(item, next) > 0) return true;
                        }

                    }
                    else
                    {
                        int index = itemIndex;
                        if (index > 0)
                        {
                            T prev = (T)Items[index - 1];
                            if (Comparer.Compare(prev, item) > 0) return true;
                        }
                        if (index < (Items.Count - 1))
                        {
                            T next = (T)Items[index + 1];
                            if (Comparer.Compare(item, next) > 0) return true;
                        }

                    }
                }
            }
            return false;
        }

        private int FindGroupBeforeItem(int itemIndex)
        {
            int groupIndex = itemIndex - 1;
            while (groupIndex >= 0)
            {
                if (Items[groupIndex] is GroupItem) break;
                groupIndex--;
            }
            return groupIndex;
        }

        /// <summary>
        /// Gets the group name fom an item.
        /// </summary>
        /// <param name="item">Item for which to get the group key.</param>
        /// <returns>Group name for the specified item.</returns>
        protected abstract string GetGroupName(T item);


        /// <summary>
        /// Clears the collection
        /// </summary>
        public void Clear()
        {
            if (IsObserved)
            {
                foreach (var item in Items)
                {
                    (item as INotifyPropertyChanged).PropertyChanged -= HandlePropertyChanged;
                }
            }
            if (Items.Count > 0)
            {
                Items.Clear();
                NotifyProps(1);
            }
            groups.Clear();
        }

        /// <summary>
        /// Gets whether the data source contains an item.
        /// </summary>
        /// <param name="item">Item to check.</param>
        /// <returns>True if the item is contained in the data source, otherwise false.</returns>
        public bool Contains(T item)
        {
            return Items.Contains(item);
        }

        /// <summary>
        /// Removes an item from the collection.
        /// </summary>
        /// <param name="item">Item to remove.</param>
        /// <returns>True, if removed, otherwise false.</returns>
        public bool Remove(T item)
        {
            int index = Items.IndexOf(item);
            if (index >= 0)
            {
                int prevCount = Count;
                Items.RemoveAt(index);
                if (CanNotify && !IsObserved) (item as INotifyPropertyChanged).PropertyChanged -= HandlePropertyChanged;
                int groupIndex = FindGroupBeforeItem(index);

                GroupItem group = groupIndex >= 0 ? Items[groupIndex] as GroupItem : null;
                if (group != null)
                {
                    group.ChildCount--;
                    if (group.ChildCount == 0)
                    {
                        Items.RemoveAt(groupIndex);
                        groups.Remove(group.GroupName);
                    }
                }
                NotifyProps(prevCount);
                return true;
            }
            else return false;
        }

        /// <summary>
        /// Gets the group for a specified item.
        /// </summary>
        /// <param name="item">Item for which to get the group.</param>
        /// <returns>Group for the specified item, otherwise null.</returns>
        protected virtual GroupItem GetGroupItem(T item)
        {
            GroupItem group;
            string key = GetGroupName(item);
            if (key == null) return null;
            return Groups.TryGetValue(key, out group) ? group : null;
        }

        private int GetInsertGroupIndex(string key)
        {
            int index = 0;

            while (index < Items.Count)
            {
                GroupItem group = Items[index] as GroupItem;

                if (string.CompareOrdinal(group.GroupName, key) >= 0) return index;

                index += 1 + group.ChildCount;
            }
            return Items.Count;
        }

        private int GetInsertIndex(int groupIndex, GroupItem group, T item)
        {
            //return groupIndex + group.ChildCount;

            int childCount = group.ChildCount - 1;
            if (childCount == 0) return groupIndex + 1;

            IComparer<T> comparer = (IComparer<T>)Comparer ?? Comparer<T>.Default;
            int index = BinarySearchUtil.BinarySearch(Items, groupIndex + 1, childCount, item, comparer);
            if (index < 0) index = ~index;
            return index;
        }

        private int GetInsertIndex(T item)
        {
            IComparer<T> comparer = (IComparer<T>)Comparer ?? Comparer<T>.Default;
            int index = BinarySearchUtil.BinarySearch(Items, 0, Items.Count, item, comparer);
            if (index < 0) index = ~index;
            return index;
        }

        /// <summary>
        /// Populates the Items collection and adding necassary GroupItems from a specified collection.
        /// </summary>
        /// <param name="items">Collection to populate.</param>
        /// <param name="comparer">Comparer to be used for comparing two items.</param>
        protected abstract void Populate(IEnumerable<T> items, IComparer<T> comparer);


        void ICollectionChangedListener.OnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    Add((T)e.NewItems[0]);
                    break;

                case NotifyCollectionChangedAction.Remove:
                    var item = (T)e.OldItems[0];
                    if (CanNotify) (item as INotifyPropertyChanged).PropertyChanged -= HandlePropertyChanged;
                    Remove(item);
                    break;

                case NotifyCollectionChangedAction.Reset:
                    Clear();
                    Populate(sender as IEnumerable<T>, Comparer);
                    NotifyProps(0);
                    break;

            }
        }

        private void NotifyProps(int previousCount)
        {
            int count = Count;
            if (previousCount != count)
            {
                OnPropertyChanged("Count");
                if (previousCount == 0 || count == 0) OnPropertyChanged("IsEmpty");
            }
        }

        protected void AddInternal(T item)
        {
            if (CanNotify) (item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(HandlePropertyChanged);
            Items.Add(item);
        }

        public void PrepareItem(T item)
        {
            //  if (CanNotify) WeakNotifyPropertyChangedListener.CreateIfNecessary(item, this);
            if (CanNotify) (item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(HandlePropertyChanged);
        }

        private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (HasOrderChanged(e.PropertyName)) Reorder((T)sender); else Update((T)sender);
        }

        /// <summary>
        /// Returns true if propertyName is the name of the property which determines the order of the item in the collection.
        /// </summary>
        /// <param name="propertyName">name of the property.</param>
        protected abstract bool HasOrderChanged(string propertyName);

        public void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (HasOrderChanged(e.PropertyName)) Reorder((T)sender); else Update((T)sender);
        }

        /// <summary>
        /// Unsubscribes all items from the original source from notifying property changes.
        /// </summary>
        public void Unsubscribe()
        {
            if (CanNotify)
            {
                CanNotify = false;
                if (source != null)
                {
                    foreach (var item in source) (item as INotifyPropertyChanged).PropertyChanged -= HandlePropertyChanged;
                }
            }
        }

        /// <summary>
        /// Subscribes all items in Source for notifying this data source when a property changes.
        /// </summary>
        /// <returns>True if subscription was successfull, otherwise false.</returns>
        public bool Subscripe()
        {
            if (source != null && !CanNotify && typeof(INotifyPropertyChanged).IsAssignableFrom(this.GetType().GetGenericArguments()[0]))
            {
                CanNotify = true;
                foreach (var item in source) (item as INotifyPropertyChanged).PropertyChanged += HandlePropertyChanged;
                return true;
            }
            return false;
        }

        protected void OnPropertyChanged(string propName)
        {
            var eh = PropertyChanged;
            if (eh != null) eh(this, new PropertyChangedEventArgs(propName));
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}
